#ifndef WEB_HPP
#define WEB_HPP

#include <QApplication>
#include <QScreen>
#include <QMainWindow>
#include <QCloseEvent>
#include <QUrl>
#include <QDialog>
#include <QVBoxLayout>
#include <QBuffer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QWebEngineView>
#include <QWebEngineProfile>
#include <QWebEngineUrlSchemeHandler>
#include <QWebEngineUrlRequestJob>
#include <cmath>
#include "util.hpp"


#define WebViewContent "qrc:/qtbinding/web/content.html"
#define WebAssetScheme "asset"

class DomUpdateNotifier {
public:
    virtual ~DomUpdateNotifier() {};
    virtual void notifyDomUpdate() = 0;
};

class WebBridge final: public QObject {
    Q_OBJECT
private:
    QWebEngineView* view;
public:
    WebBridge(QWebEngineView* parent): QObject(parent), view(parent) {};
private:
    void RunJS(QString method, QJsonObject args, const QWebEngineCallback<const QVariant&>& cb) {
        QString msg = QString::fromUtf8(QJsonDocument(args).toJson());
        QString script = QString("(WebBridge.%1(%2),true)").arg(method, msg);
        view->page()->runJavaScript(script, cb);
    }
public:
    void UpdateRootFontSize(double size) {
        QJsonObject args;
        args["size"] = qreal(size);
        RunJS("UpdateRootFontSize", args, [](const QVariant&) {});
    }
    void InjectCSS(QString path) {
        QJsonObject args;
        args["path"] = path;
        RunJS("InjectCSS", args, [](const QVariant&) {});
    }
    void InjectJS(QString path) {
        QJsonObject args;
        args["path"] = path;
        RunJS("InjectJS", args, [](const QVariant&) {});
    }
    void InjectTTF(QString path, QString family, QString weight, QString style) {
        QJsonObject args;
        args["path"] = path;
        args["family"] = family;
        args["weight"] = weight;
        args["style"] = style;
        RunJS("InjectTTF", args, [](const QVariant&) {});
    }
    void CallMethod(QString id, QString method, QVariantList args) {
        QJsonObject bridge_args;
        bridge_args["id"] = id;
        bridge_args["method"] = method;
        bridge_args["args"] = QJsonArray::fromVariantList(args);
        RunJS("CallMethod", bridge_args, [](const QVariant&) {});
    }
    void PatchActualDOM(QString data, DomUpdateNotifier* notifier) {
        QJsonObject args;
        args["data"] = data;
        RunJS("PatchActualDOM", args, [notifier](const QVariant& v) {
            if (v.isValid()) {
                notifier->notifyDomUpdate();
            }
        });
    }
};

struct WebAsset {
    bool        exists;
    QString     mime;
    QByteArray  content;
};

class WebAssetStore final: public QObject {
    Q_OBJECT
    QHash<QString,WebAsset> mapping;
public:
    WebAssetStore(QObject* parent): QObject(parent) {};
    void InsertItem(QString key, QString mime, QByteArray data) {
        // qDebug() << "register" << path << "(" << data.size() << ")";
        mapping[key] = { true, mime, data };
    };
    WebAsset LookupItem(QString key) {
        return mapping.value(key);
    };
};

class WebAssetSchemeHandler: public QWebEngineUrlSchemeHandler {
    Q_OBJECT
private:
    WebAssetStore* store;
public:
    WebAssetSchemeHandler(QObject* parent, WebAssetStore* store):
        QWebEngineUrlSchemeHandler(parent), store(store) {};
    void requestStarted(QWebEngineUrlRequestJob* req) {
        QUrl url = req->requestUrl();
        QString key = DecodeBase64(url.path());
        WebAsset asset = store->LookupItem(key);
        if (asset.exists) {
            QByteArray* data = new QByteArray(asset.content);
            QBuffer* buf = new QBuffer(data, nullptr);
            QByteArray* contentType = new QByteArray(asset.mime.toUtf8());
            req->reply(*contentType, buf);
            connect(req, &QObject::destroyed, [contentType,buf,data] ()->void {
                delete contentType;
                delete buf;
                delete data;
            });
        } else {
            req->fail(QWebEngineUrlRequestJob::UrlNotFound);
        }
    }
};

class WebViewInterface {
public:
    virtual ~WebViewInterface() {};
    virtual void emitEvent(QString handler, QVariantMap payload) = 0;
    virtual void finishLoad() = 0;
};

class WebViewPage final: public QWebEnginePage {
    Q_OBJECT
private:
    WebViewInterface* view;
public:
    WebViewPage(QWebEngineProfile* profile, QObject* parent, WebViewInterface* view):
        QWebEnginePage(profile, parent), view(view) {}
    void javaScriptAlert(const QUrl &securityOrigin, const QString &msg) override {
        // We abuse the blocking nature of the alert() function to
        // achieve synchronous IPC between Qt and Web Contents.
        static_cast<void>(securityOrigin);
        QString ipc_prefix = "IPC:";
        if (msg.startsWith(ipc_prefix)) {
            QString ipc_payload = msg.mid(ipc_prefix.length());
            QJsonDocument doc = QJsonDocument::fromJson(ipc_payload.toUtf8());
            if (doc.isNull()) { return; }
            auto obj = doc.object();
            auto method = obj["method"].toString();
            if (method == "finishLoad") {
                view->finishLoad();
            } else if (method == "emitEvent") {
                auto args = obj["args"].toObject();
                QString handler = args["handler"].toString();
                QVariantMap payload = args["payload"].toObject().toVariantMap();
                view->emitEvent(handler, payload);
            }
        }
    }
    bool javaScriptConfirm(const QUrl &securityOrigin, const QString &msg) override {
        static_cast<void>(securityOrigin);
        static_cast<void>(msg);
        // reserved
        return false;
    }
    bool javaScriptPrompt(const QUrl &securityOrigin, const QString &msg, const QString &defaultValue, QString *result) override {
        static_cast<void>(securityOrigin);
        static_cast<void>(msg);
        static_cast<void>(defaultValue);
        static_cast<void>(result);
        // reserved
        return false;
    }
    void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceID) override {
        QWebEnginePage::javaScriptConsoleMessage(level, message, lineNumber, sourceID);
        // reserved
    }
};

class WebView final: public QWebEngineView, public WebViewInterface, public DomUpdateNotifier {
    Q_OBJECT
private:
    bool debug;
public:
    WebView(QWidget* parent, bool debug): QWebEngineView(parent), debug(debug) {
        initPage();
        initBridge();
        setContextMenuPolicy(Qt::NoContextMenu);
    }
    ~WebView() {}
    WebBridge* getBridge() {
        return bridge;
    }
    WebAssetStore* getStore() {
        return store;
    }
public:
    void LoadContent() {
        setUrl(QUrl(WebViewContent));
        if (debug) {
            openInspector();
        }
    }
signals:
    void loadFinished();
    void eventEmitted();
    void domUpdated();
public:
    void emitEvent(QString handler, QVariantMap payload) override {
        emittedEventHandler = handler;
        emittedEventPayload = payload;
        emit eventEmitted();
        emittedEventHandler = "";
        emittedEventPayload = QVariantMap();
        // if (debug) {
        //     qDebug() << "Event: " << handler;
        // }
    }
    void finishLoad() override {
        syncRootFontSizeWithScreenSize();
        emit loadFinished();
    }
    void notifyDomUpdate() override {
        emit domUpdated();
    }
private:
    QString emittedEventHandler;
    QVariantMap emittedEventPayload;
public:
    QString getEmittedEventHandler() const { return emittedEventHandler; }
    QVariantMap getEmittedEventPayload() const { return emittedEventPayload; }
private:
    WebAssetStore* store = nullptr;
    void initPage() {
        store = new WebAssetStore(this);
        WebAssetSchemeHandler* handler = new WebAssetSchemeHandler(this, store);
        QWebEngineProfile* profile = new QWebEngineProfile((QObject*) nullptr);
        profile->installUrlSchemeHandler(WebAssetScheme, handler);
        WebViewPage* page = new WebViewPage(profile, this, this);
        // page should be deleted before the profile being deleted
        profile->setParent(page);
        setPage(page);
    };
private:
    WebBridge* bridge;
    void initBridge() {
        bridge = new WebBridge(this);
    }
    void syncRootFontSizeWithScreenSize() {
        updateRootFontSize();
        QScreen *screen = QGuiApplication::primaryScreen();
        connect(screen, &QScreen::geometryChanged, this, &WebView::updateRootFontSize);
    }
    void updateRootFontSize() {
        int fontSize = Get1remPixels();
        bridge->UpdateRootFontSize(double(fontSize));
    }
private:
    QDialog* inspector_dialog = nullptr;
    void openInspector() {
        if (inspector_dialog != nullptr) {
            return;
        }
        QWebEngineView* inspector = new QWebEngineView(this);
        inspector->page()->setInspectedPage(this->page());
        inspector_dialog = new QDialog(this);
        inspector_dialog->setLayout(new QVBoxLayout(inspector_dialog));
        inspector_dialog->layout()->addWidget(inspector);
        inspector_dialog->setModal(false);
        inspector_dialog->resize(800, 360);
        inspector_dialog->layout()->setContentsMargins(0, 0, 0, 0);
        inspector_dialog->setWindowTitle(tr("WebEngine Inspector"));
        inspector_dialog->show();
        inspector_dialog->raise();
        MoveToScreenCenter(inspector_dialog);
    }
};

class WebWindow final: public QMainWindow {
    Q_OBJECT
private:
    size_t kind;
    bool closeBtn;
    WebView* view;
public:
    WebWindow(QWidget* parent, size_t kind, QIcon icon, QString title, QSize size_rem, bool closeBtn, bool modal, bool debug): QMainWindow(parent), kind(kind), closeBtn(closeBtn) {
        view = new WebView(this, debug);
        layout()->setContentsMargins(0, 0, 0, 0);
        setCentralWidget(view);
        setWindowIcon(icon);
        setWindowTitle(title);
        resize(GetSizeFromRelative(size_rem));
        Qt::WindowFlags flags = Qt::Widget;
        if (kind == QtWindowKindOrdinary || kind == QtWindowKindDialog) {
            flags |= Qt::CustomizeWindowHint;
            flags |= Qt::WindowTitleHint;
            flags |= Qt::WindowMinimizeButtonHint;
            if (closeBtn) {
                flags |= Qt::WindowCloseButtonHint;
            }
            if (kind == QtWindowKindOrdinary) {
                flags |= Qt::WindowMaximizeButtonHint;
                flags |= Qt::Window;
            } else if (kind == QtWindowKindDialog) {
                flags |= Qt::Dialog;
            }
        } else if (kind == QtWindowKindPopup) {
            // NOTE: When using Qt::Popup,
            //       weird focus handling behavior is observed.
            //       The orange focus outline is missing when the popup is opened.
            //       It seems that the popup is in a "semi-focus" state.
            //       A click is required to make it work normally.
            //       Therefore, we use the following flags instead.
            flags |= Qt::Window;
            flags |= Qt::FramelessWindowHint;
            flags |= Qt::WindowStaysOnTopHint;
        }
        setWindowFlags(flags);
        if (modal) {
            setWindowModality(Qt::ApplicationModal);            
        }
    }
    ~WebWindow() {}
    WebView* getWebView() {
        return view;
    }
    void showRelative(bool set_pos, QPoint pos = QPoint(0,0)) {
        QWidget* active = QApplication::activeWindow();
        WebWindow* w = qobject_cast<WebWindow*>(active);
        if (w != nullptr) {
            if (!(set_pos)) {
                // use the center of the active window
                pos = (w->rect().center() - rect().center());
            }
            move(w->mapToGlobal(pos));
        } else {
            MoveToScreenCenter(this);
        }
        show();
        activateWindow();
        raise();
    }
signals:
    void deactivated();
protected:
    void changeEvent(QEvent* ev) override {
        if (ev->type() == QEvent::ActivationChange && !(this->isActiveWindow())) {
            emit deactivated();
        }
        QMainWindow::changeEvent(ev);
    }
    void closeEvent(QCloseEvent* ev) override {
        // it is not clear whether Qt::CustomizeWindowHint works on all platforms
        if (closeBtn) {
            ev->accept();
        } else {
            ev->ignore();
        }
    }
};

#endif  // WEB_HPP

